Skip to content

Add optional mTLS support to the tang pin#566

Merged
sergio-correia merged 1 commit into
latchset:masterfrom
liyue32:tang-mtls-support
Jul 25, 2026
Merged

Add optional mTLS support to the tang pin#566
sergio-correia merged 1 commit into
latchset:masterfrom
liyue32:tang-mtls-support

Conversation

@liyue32

@liyue32 liyue32 commented Jul 7, 2026

Copy link
Copy Markdown

Summary

Adds optional mutual-TLS (mTLS) support to the Tang pin, allowing Clevis to authenticate to a Tang server (typically fronted by a TLS-terminating reverse proxy) with a client certificate, and to verify the server certificate against a custom CA.
The feature is fully opt-in: when none of the new properties are set, the pin behaves exactly as before (plain HTTP/HTTPS with the system's default CA trust). Existing JWEs and workflows are unaffected.

Motivation

Adding mTLS lets the Tang server (typically via a TLS-terminating reverse proxy) authenticate the client's identity through its client certificate. This unlocks per-machine access control on top of the existing network-binding model. In particular, single-machine revocation: revoking or refusing a specific client certificate immediately prevents that one machine from recovering its keys, without affecting any other machine or requiring key rotation on the server. This matches how we want to adopt Clevis out of the box at Meta: bind machines to Tang for automated unlock, and retain the ability to revoke an individual host's recovery access by its identity. It's also useful more generally for any deployment where the Tang server is fronted by HTTPS with client-certificate authentication, or uses a server certificate signed by a private/internal CA.

What changed

Three new optional config properties are accepted by clevis encrypt tang:

  1. cacert: curl --cacert
  2. cert: curl --cert
  3. key: curl --key

When any of above are set:

  1. passed to curl when fetching the advertisement during encryption, and
  2. persisted into the JWE protected header under clevis.tang, so that clevis decrypt reuses them for the POST /rec request without any additional configuration (Clevis decryption takes no config by design, so this metadata must travel in the JWE).

Example:

  clevis encrypt tang '{
    "url":"https://tang.example.com",
    "cacert":"/etc/clevis/ca.crt",
    "cert":"/etc/clevis/client.crt",
    "key":"/etc/clevis/client.key"
  }' < PT > JWE

Backward compatibility

  • When no mTLS properties are set, the injected curl option array is empty, so curl is invoked identically to before.
  • Existing JWEs have no mTLS header fields; decryption reads them as absent and takes the unchanged code path.

Tests

  • Adds a new pin-tang-mtls integration test plus tang_generate_certs, tang_run_mtls, and tang_mtls_sanity_check helpers. The harness fronts tangd with a TLS listener (socat OPENSSL-LISTEN) that mandates client-certificate verification. The test covers:
    • Positive: encrypt (GET /adv) + decrypt (POST /rec) round-trip succeed over mTLS. Since the server enforces client-cert verification, success proves the pin actually presents the client certificate.
    • Header round-trip: the cacert/cert/key paths are correctly persisted into and read back from the JWE protected header.
    • Negative: without a client certificate, encryption fails — proving mTLS is enforced, not silently downgraded.

@sarroutbi

Copy link
Copy Markdown
Collaborator

/packit test

@liyue32

liyue32 commented Jul 13, 2026

Copy link
Copy Markdown
Author

@sarroutbi I could not see the detailed error message of centos-stream-10-x86_64:pkcs11 failure, but seems like it is timeout issue, could you help to take a look if this is related with my PR changes?

@sergio-correia

Copy link
Copy Markdown
Collaborator

/packit retest-failed

Comment thread src/pins/tang/tests/pin-tang-mtls
Comment thread src/pins/tang/clevis-encrypt-tang

# Optional (m)TLS parameters. Absent for JWEs created without mTLS, in which
# case curl behaves exactly as before.
cacert="$(jose fmt -j- -Og clevis -g tang -g cacert -Su- <<< "$jhd")" || true

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During decryption, cacert, cert, key (and url) are all extracted from the JWE protected header and fed to curl (line 115) before the AEAD integrity check at line 128 (jose jwe dec). The url field was already consumed unauthenticated before this patch, so this is a pre-existing design property of clevis-tang. However, cacert qualitatively extends the attack surface: an attacker who can modify the stored JWE can now also override which CA curl trusts, enabling MITM against the legitimate tang server with a self-signed cert planted on disk. Previously, modifying url alone still required a certificate trusted by the system CA store.

The practical exploitability is limited (requires disk write access + network position, and the ECMR exchange value alone does not reveal the decryption key), so this may be acceptable as a known trade-off.

Worth considering: validate that cert/key/cacert paths are under an expected prefix (e.g., /etc/clevis/), or use indirection (store a profile name, resolve to paths at runtime from a fixed directory). At minimum, document the trust model explicitly so users understand the implication.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for your review! In my understanding, if attacker could modify JWE header, they could probably also modify the ca trust store in /etc/pki. If this is correct, cacert does not add more security risk here (correct me if my understanding is wrong). I also thought about adding validation for cert/key/cacert paths, but considering this aims to be used generically, it is hard to add any specific cacert path validation so I feel path-prefix validation felt too restrictive here.
I added one section named "mTLS SECURITY MODEL" in clevis-encrypt-tang.1.adoc to describe the potential risk of cacert and recommende that the CA be installed in the system trust store (with cacert omitted) where possible

Comment thread src/pins/tang/clevis-decrypt-tang
Comment thread src/pins/tang/clevis-encrypt-tang
@liyue32
liyue32 force-pushed the tang-mtls-support branch from 84553f4 to 3bc2cb7 Compare July 15, 2026 04:22

@liyue32 liyue32 left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

address the comments by

  • add cert, key and cacert path validation
  • Add security model document of using cacert

@liyue32
liyue32 requested a review from sergio-correia July 16, 2026 21:30
@liyue32

liyue32 commented Jul 21, 2026

Copy link
Copy Markdown
Author

@sarroutbi @sergio-correia I addressed all the previous comments, would you mind taking a look again? Thanks!

== mTLS SECURITY MODEL

The *cacert*, *cert* and *key* properties are stored as plaintext in the JWE
header without authentication, so there is a chance they could be modified.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wording here says the mTLS properties are stored "without authentication", but they're placed in the JWE protected header (lines 193-195 of the encrypt script), which is the AAD for AES-256-GCM per RFC 7516 Section 5.1. Modifying any field in the protected header causes jose jwe dec to reject the token.

The real subtlety is that clevis-decrypt-tang reads and acts on these values (the curl request at line 123) before the AEAD integrity check runs (line 136). So tampering is detected, but only after one network request has already been made with the tampered parameters.

Would something like this be more accurate?

Suggested change
header without authentication, so there is a chance they could be modified.
The *cacert*, *cert* and *key* properties are stored in the JWE *protected*
header, which is integrity-protected by the AES-256-GCM authentication tag.
Modifying any of these values will cause decryption to fail. However, the
integrity check is only performed *after* the values have been used to contact
the Tang server (the decryption key is needed to verify the tag, and the Tang
server must be contacted to derive that key). This means a tampered JWE will
cause the system to fail to unlock, but one curl request will have been sent
using the tampered parameters.

[ -n "$key" ] && [ ! -f "$key" ] && { echo "Client key file '$key' not found!" >&2; exit 1; }

# 'key' on its own is a no-op: curl ignores --key unless --cert is also given.
[ -n "$key" ] && [ -z "$cert" ] && { echo "'key' requires 'cert' to also be set!" >&2; exit 1; }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When cacert, cert, or key are set but url uses http://, curl silently ignores --cacert, --cert, and --key. Encryption succeeds, the paths get baked into the JWE header, but all traffic goes over plaintext. The user thinks mTLS is protecting the connection when it isn't.

Would it make sense to reject this at bind time, similar to the key-without-cert check right above?

if [ -n "$cacert" ] || [ -n "$cert" ]; then
    case "$url" in
        https://*) ;;
        *) echo "TLS options (cacert/cert/key) require an https:// URL!" >&2
           exit 1 ;;
    esac
fi

@sarroutbi

Copy link
Copy Markdown
Collaborator

/packit test

Allow the tang pin to authenticate to the Tang server with mutual TLS.
Three new optional config properties are accepted by clevis-encrypt-tang:

  cacert - CA bundle to verify the server certificate (curl --cacert)
  cert   - client certificate for mTLS                (curl --cert)
  key    - private key for the client certificate     (curl --key)

When none are set, behaviour is unchanged. The paths are persisted into
the JWE protected header so clevis-decrypt-tang reuses them for the
recovery (POST /rec) request without extra configuration.

Add a pin-tang-mtls integration test (TLS-fronted tangd via socat with
mandatory client-cert verification) plus tang_generate_certs and
tang_run_mtls helpers. The test skips cleanly when openssl or a
socat built with OpenSSL support is unavailable.
@liyue32
liyue32 force-pushed the tang-mtls-support branch from 3bc2cb7 to e1de291 Compare July 25, 2026 06:16
@sergio-correia

Copy link
Copy Markdown
Collaborator

/packit test

@sergio-correia sergio-correia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@sergio-correia
sergio-correia merged commit 1c9e927 into latchset:master Jul 25, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants